using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Configuration;
using System.Xml;
using System.Net;
using MIS.EN.WebDav;
using MIS.EN.Exchange.Entities;
using MIS.EN.Exchange.Parsers;
using System.Reflection;
using Evolution.Business.Specifications;
using MIS.EN.Exchange.PropertyMappings;

namespace MIS.EN.Exchange
{
    internal abstract class FolderWorker<T> where T : Item
    {
        protected abstract void BuildContentClassNode(StringBuilder xmlBuilder);
        protected abstract void BuildMessageClass(StringBuilder xmlBuilder);
        protected abstract string FolderName { get; }
        protected abstract string PropertyMappingsFileName { get; }
        protected abstract Type ItemType { get; }
        protected abstract bool UsesHexadecimalXmlNames { get; }
        
        private WebDavCommand command;
        private Dictionary<string, string> webDavDocumentNamespaces = null;
        private List<PropertyMapping> propertyMappings = null;

        protected FolderWorker(NetworkCredential credentials, bool useFormsBasedAuthentication, 
            bool useIsaServer2006)
        {
            this.command = new WebDavCommand(credentials, useFormsBasedAuthentication, 
                useIsaServer2006, this.UsesHexadecimalXmlNames);
            this.webDavDocumentNamespaces = new Dictionary<string, string>();
            this.LoadPropertyMappings();
        }

        protected Dictionary<string, string> WebDavDocumentNamespaces
        {
            get { return this.webDavDocumentNamespaces; }
        }

        protected IList<PropertyMapping> PropertyMappings
        {
            get { return this.propertyMappings; }
        }

        /// <summary>
        /// Creates/Updates an Exchange folder item from data in the <see cref="MIS.EN.Exchange.Entities.Item"/>.
        /// </summary>
        /// <param name="item">An <see cref="MIS.EN.Exchange.Entities.Item"/> that represents the Exchange item.</param>
        /// <param name="userMailBox"></param>
        public void Save(Item item, Uri userMailBox)
        {
            string itemUri = string.Empty;

            // Build the URI of the item
            if (item.Href.Length > 0)
            {
                itemUri = item.Href;
            }
            else
            {
                itemUri = string.Format("{0}/{1}/{2}.eml", userMailBox.AbsoluteUri, this.FolderName, item.Name);
            }

            // Send the XML request to Exchange via WebDAV
            this.command.ExecuteNonQuery(new Uri(itemUri), WebDavCommand.WebDavMethod.PropPatch, this.SerializeItem(item));
            item.Href = itemUri;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="item">An <see cref="MIS.EN.Exchange.Entities.Item"/> that represents the Exchange item.</param>
        /// <returns></returns>
        public string SerializeItem(Item item)
        {
            StringBuilder xmlBody = new StringBuilder(500);

            // Build the processing instruction
            xmlBody.Append("<?xml version=\"1.0\"?>");

            // Build the XML namespaces
            this.BuildNamespaces(false);

            // Build the property update tag
            this.BuildPropertyUpdateOpeningTag(xmlBody);

            // Build the set and prop tags
            xmlBody.Append(@"<a:set><a:prop>");

            // The content class and message class are the two required fields

            // content class
            xmlBody.Append("<a:contentclass>");
            this.BuildContentClassNode(xmlBody);
            xmlBody.Append("</a:contentclass>");

            // message class
            xmlBody.Append("<e:outlookmessageclass>");
            this.BuildMessageClass(xmlBody);
            xmlBody.Append("</e:outlookmessageclass>");

            // Fill in the field values
            this.SerializeProperties(xmlBody, item);

            // Close the set and prop tags for the property update
            xmlBody.Append(@"</a:prop></a:set>");
            
            // Close the property update tag
            xmlBody.Append(@"</a:propertyupdate>");

            return xmlBody.ToString();
        }

        /// <summary>
        /// Deletes the Exchange item.
        /// </summary>
        /// <param name="item">An <see cref="MIS.EN.Exchange.Entities.Item"/> that represents the Exchange item.</param>
        public void Delete(Item item)
        {
            // Send the request to Exchange via WebDAV
            this.command.ExecuteNonQuery(new Uri(item.Href), WebDavCommand.WebDavMethod.Delete, null);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="uri"></param>
        /// <param name="credentials"></param>
        /// <param name="useFormsBasedAuthentication"></param>
        /// <param name="useIsaServer2006"></param>
        public static void Delete(Uri uri, NetworkCredential credentials, bool useFormsBasedAuthentication, bool useIsaServer2006)
        {
            // Send the request to Exchange via WebDAV
            WebDavCommand command = new WebDavCommand(credentials, useFormsBasedAuthentication, useIsaServer2006, false);
            command.ExecuteNonQuery(uri, WebDavCommand.WebDavMethod.Delete, null);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="uri"></param>
        /// <param name="credentials"></param>
        public static void Delete(Uri uri, NetworkCredential credentials)
        {
            FolderWorker<T>.Delete(uri, credentials, false, false);
        }

        /// <summary>
        /// Gets the filtered set of items from a folder based on the specification for a given Exchange user.  
        /// Optionally uses WebDAV replication.
        /// </summary>
        /// <returns>An list of folder items.</returns>
        public IList<T> Retrieve(Uri userMailBox, bool useWebDavReplication, CompositeSpecification<T> specification)
        {
            // Build the URI of the user's folder that we are interested in
            Uri uri = new Uri(string.Format("{0}/{1}", userMailBox.AbsoluteUri, this.FolderName));

            // Start building the WebDAV query to send to Exchange
            StringBuilder xmlBody = new StringBuilder("<?xml version='1.0' ?><a:searchrequest xmlns:a='DAV:' ");

            // If necessary, add the proper XML for WebDAV replication
            /*
             * if using replication then...
             * xmlns:d='http://schemas.microsoft.com/repl/'
             * try to get the collblob for the particular user's folder
             * <d:repl>
                  <d:collblob/>
               </d:repl>
             */
            if (useWebDavReplication)
            {
                xmlBody.Append("xmlns:d='http://schemas.microsoft.com/repl/'");
                xmlBody.Append(">");
                xmlBody.Append("<d:repl><d:collblob>");
                xmlBody.Append(WebDavCollBlobHelper.RetrieveCollBlob(userMailBox.AbsolutePath, this.FolderName));
                xmlBody.Append("</d:collblob></d:repl>");
            }
            else
            {
                xmlBody.Append(">");
            }

            // Start the WebDAV SQL statement
            xmlBody.Append("<a:sql>SELECT ");
            
            // Get the fields that we want to be in the resultset
            this.CreateSearchFields(xmlBody);
            
            // Always get the date last modified
            xmlBody.Append("\"DAV:getlastmodified\"");

            // Generate the rest of the query
            xmlBody.Append(" FROM scope('shallow traversal of \"" + uri.AbsoluteUri);
            xmlBody.Append("\"') WHERE \"DAV:isfolder\"=False ");

            // Build the filter from the specification passed in
            if (specification != null)
            {
                this.BuildFilter(specification, xmlBody);
            }

            // Close the WebDAV sql statement
            xmlBody.Append("</a:sql>");

            // Close the WebDAV searchrequest
            xmlBody.Append("</a:searchrequest>");

            // Send the XML request to Exchange via WebDAV, then deserialize the XML into a List<T>
            return this.DeserializeSearchResults(this.command.ExecuteXmlDocument(uri,
                WebDavCommand.WebDavMethod.Search, xmlBody.ToString()), userMailBox, useWebDavReplication);
        }

        private void BuildFilter(CompositeSpecification<T> specification, StringBuilder xmlBody)
        {
            // Only support AND operations for now
            string schemaShortName = string.Empty;
            IWebDavResultValueParser parser = null;
            foreach (ISpecification<T> component in specification.Components)
            {
                if (component is ISpecificationParameter)
                {
                    xmlBody.Append("AND (");
                    xmlBody.Append("\"");
                    xmlBody.Append(this.propertyMappings.Find(new Predicate<PropertyMapping>(delegate(PropertyMapping propertyMapping) 
                    {
                        return propertyMapping.Name == ((ISpecificationParameter)component).ParameterName; 
                    })).SchemaName);
                    xmlBody.Append("\"");
                    xmlBody.Append(" ");
                    xmlBody.Append(((IOperator)component).Operator);
                    xmlBody.Append(" ");
                    parser = WebDavResultValueParserFactory.GetWebDavResultValueParser(((ISpecificationParameter)component).ParameterType.ToString());
                    xmlBody.Append(parser.GetFormattedValueForSearch(((ISpecificationParameter)component).ParameterValue));
                    xmlBody.Append(") ");
                }
            }
        }
        
        /// <summary>
        /// Gets all of the items from a folder for a given Exchange user.  Optionally uses WebDAV replication.
        /// </summary>
        /// <returns>An list of folder items.</returns>
        public IList<T> Retrieve(Uri userMailBox, bool useWebDavReplication)
        {
            return this.Retrieve(userMailBox, useWebDavReplication, null);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="webDavResponseDocument"></param>
        /// <param name="userMailBox"></param>
        /// <param name="useWebDavReplication"></param>
        /// <returns></returns>
        private IList<T> DeserializeSearchResults(XmlDocument webDavResponseDocument, Uri userMailBox, bool useWebDavReplication)
        {
            List<T> items = new List<T>();
            this.BuildNamespaces(useWebDavReplication);
            WebDavXmlHelper webDavXmlHelper = new WebDavXmlHelper(webDavResponseDocument, useWebDavReplication, webDavDocumentNamespaces);
            XmlNodeList responses = webDavResponseDocument.SelectNodes("//a:response", webDavXmlHelper.NamespaceManager);

            // See if we need to deal with WebDAV replication
            if (useWebDavReplication)
            {
                // save the collblob if it is returned
                WebDavCollBlobHelper.PersistCollBlob(webDavXmlHelper.CollBlob, userMailBox.AbsolutePath, this.FolderName);
            }

            // Iterate through the XML to build up the List<Item>
            foreach (XmlNode response in responses)
            {
                // Make sure that we really have a record with property values
                if (this.ValidResponseNode(response, webDavXmlHelper))
                {
                    // Add the item to the list
                    items.Add(this.DeserializeProperties(response, webDavXmlHelper) as T);

                    // TODO:  Add logic to capture the last modified date here...
                }
            }
            return items;
        }

        private bool ValidResponseNode(XmlNode response, WebDavXmlHelper webDavXmlHelper)
        {
            return (webDavXmlHelper.GetStringPropertyValueFromResponseNode(response, "a", "id") != null);
        }

        protected bool ValidDateTime(string value)
        {
            DateTime parsedValue;
            return DateTime.TryParse(value, out parsedValue);
        }

        protected virtual void BuildNamespaces(bool useWebDavReplication)
        {
            this.webDavDocumentNamespaces.Add("a", "DAV:");
            this.webDavDocumentNamespaces.Add("x", "xml:");
            this.webDavDocumentNamespaces.Add("dt", "urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/");
            if (useWebDavReplication)
            {
                this.webDavDocumentNamespaces.Add("d", "http://schemas.microsoft.com/repl/");
                this.WebDavDocumentNamespaces.Add("f", "http://schemas.microsoft.com/exchange/");
            }
            else
            {
                this.WebDavDocumentNamespaces.Add("e", "http://schemas.microsoft.com/exchange/");
            }
        }

        protected void BuildPropertyUpdateOpeningTag(StringBuilder xmlBuilder)
        {
            xmlBuilder.Append("<a:propertyupdate ");
            foreach (string key in this.webDavDocumentNamespaces.Keys)
            {
                xmlBuilder.Append("xmlns:");
                xmlBuilder.Append(key);
                xmlBuilder.Append("=\"");
                xmlBuilder.Append(this.webDavDocumentNamespaces[key]);
                xmlBuilder.Append("\" ");
            }
            xmlBuilder.Append(">");
        }

        protected void CreateSearchFields(StringBuilder xmlBuilder)
        {
            xmlBuilder.Append("\"DAV:id\",");

            // Dynamically create the search field list
            foreach (PropertyMapping propertyMapping in this.propertyMappings)
            {
                xmlBuilder.Append("\"");
                xmlBuilder.Append(propertyMapping.SchemaName);
                xmlBuilder.Append("\",");
            }
        }

        protected string GetNamespaceAlias(string schemaName)
        {
            string namespaceAlias = string.Empty;
            foreach (string key in this.webDavDocumentNamespaces.Keys)
            {
                if (schemaName.Contains(this.webDavDocumentNamespaces[key]))
                {
                    namespaceAlias = key;
                    break;
                }
            }
            return namespaceAlias;
        }

        protected virtual void SerializeProperties(StringBuilder xmlBuilder, Item item)
        {
            // Dynamically get the property values
            string nameSpaceAliasRetrieve = string.Empty;
            IWebDavResultValueParser parser = null;
            Type itemType = item.GetType();
            object propertyValue = null;
            foreach (PropertyMapping propertyMapping in this.propertyMappings)
            {
                propertyValue = itemType.GetProperty(propertyMapping.Name).GetValue(item, null);
                if (propertyMapping.Required && (propertyValue == null || propertyValue.ToString().Length == 0))
                {
                    throw new ArgumentException(string.Format("Expected the {0} property to be set.", propertyMapping.Name), propertyMapping.Name);
                }
                if (propertyValue != null && (propertyValue.ToString().Length > 0 || item.PropertiesChanged.Contains(propertyMapping.Name)))
                {
                    nameSpaceAliasRetrieve = this.GetNamespaceAlias(propertyMapping.SchemaName);
                    parser = WebDavResultValueParserFactory.GetWebDavResultValueParser(propertyMapping.Type);
                    xmlBuilder.Append("<");
                    xmlBuilder.Append(nameSpaceAliasRetrieve);
                    xmlBuilder.Append(":");
                    xmlBuilder.Append(propertyMapping.SchemaShortName);
                    xmlBuilder.Append(">");
                    xmlBuilder.Append(parser.GetFormattedValue(propertyValue));
                    xmlBuilder.Append("</");
                    xmlBuilder.Append(nameSpaceAliasRetrieve);
                    xmlBuilder.Append(":");
                    xmlBuilder.Append(propertyMapping.SchemaShortName);
                    xmlBuilder.Append(">");
                }
            }
        }

        protected Item DeserializeProperties(XmlNode responseNode, WebDavXmlHelper webDavXmlHelper)
        {
            Item item = EntityFactory.BuildEntity(this.ItemType);
            item.Id = webDavXmlHelper.GetStringPropertyValueFromResponseNode(responseNode, "a", "id");
            item.Href = webDavXmlHelper.GetHrefPropertyFromResponseNode(responseNode);

            // Dynamically set the property values
            Type itemType = item.GetType();
            string nameSpaceAliasRetrieve = string.Empty;
            string schemaShortName = string.Empty;
            IWebDavResultValueParser parser = null;
            foreach (PropertyMapping propertyMapping in this.propertyMappings)
            {
                schemaShortName = propertyMapping.SchemaShortName;
                if (this.UsesHexadecimalXmlNames)
                {
                    if (schemaShortName.StartsWith("0x"))
                    {
                        schemaShortName = schemaShortName.Remove(0, 1);
                    }
                }
                nameSpaceAliasRetrieve = this.GetNamespaceAlias(propertyMapping.SchemaName);
                parser = WebDavResultValueParserFactory.GetWebDavResultValueParser(propertyMapping.Type);
                itemType.GetProperty(propertyMapping.Name).SetValue(item,
                    parser.ParseValue(responseNode, webDavXmlHelper, nameSpaceAliasRetrieve,
                    schemaShortName, propertyMapping.Type), null);
            }
            item.DoneLoadingFromServer = true;
            return item;
        }

        private void LoadPropertyMappings()
        {
            // Initialize the property mapping list
            this.propertyMappings = new List<PropertyMapping>();

            // Load the property mappings file
            XmlDocument mappings = new XmlDocument();
            mappings.Load(Assembly.GetExecutingAssembly().GetManifestResourceStream(this.PropertyMappingsFileName));

            // Get the properties
            XmlNodeList propertyNodes = mappings.SelectNodes("/propertyMappings/property");

            // Load the property mapping list
            PropertyMapping propertyMapping = null;
            foreach (XmlNode propertyNode in propertyNodes)
            {
                propertyMapping = new PropertyMapping(propertyNode.Attributes.GetNamedItem("name").Value, 
                    propertyNode.Attributes.GetNamedItem("schemaName").Value, 
                    propertyNode.Attributes.GetNamedItem("schemaShortName").Value,
                    propertyNode.Attributes.GetNamedItem("type").Value,
                    (propertyNode.Attributes.GetNamedItem("required").Value.ToLower() == "true"));
                this.propertyMappings.Add(propertyMapping);
            }
        }
    }
}
